Fix RHEL10 to be semantically identical to Ubuntu HWE 6.17 - #209
Open
hbirth wants to merge 24 commits into
Open
Conversation
The interval tree recorded a grant as a node, and every path that touched the page cache had to keep the tree in step with it: a revoked range was kept so writeback would take the grant again, and each place that dropped folios then had to free the ranges the drop had emptied. That is a second description of the page cache, maintained by hand at every drop site, and wrong wherever a site was missed. Record the grants in a bitmap instead, a bit per page, in fixed shards by offset. Adjacent grants coalesce by setting neighbouring bits, so nothing is allocated or rearranged to record one, and a revoke clears bits rather than splitting nodes. The record then says what the server has given this client and nothing about the page cache: a revoked grant is forgotten, not kept, because writeback holds the range again for every run it sends, and an absent record and a revoked one both make it ask. Every fuse_dlm_ranges_dropped() site goes with it, and so does fuse_dlm_write_grant_exists(): with grants forgotten on revoke there is no cheap answer to "is anything held for write", and the dirty and writeback tags already say what the attribute cache needs. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
write_cache_pages() takes the dirty flag off a folio before it calls ->writepage, and fuse_launder_folio() does the same for the submit it makes itself, so a folio that comes back unsent has thrown its bytes away unless they are put back. Every path here already puts them back, but none of them asks whether there is still a connection to send them on: after an abort every send fails, and a folio redirtied for a retry that can no longer happen keeps sync() going forever. Put the redirty in one helper and make it ask. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_attr_cache_mask() decides whether the server's size wins from an i_size it reads on entry, and then sleeps in the grant query before fuse_change_attributes_i() applies the answer under fi->lock. A GETATTR that left while i_size still matched the server's is therefore applied over every extension made since, and i_size drops by exactly the writes in flight. truncate_pagecache() then zeroes the tail of the page holding the new size and drops what is above, which writeback sends as zeros. Move attr_version where fuse_write_end() commits the extension so those replies are dropped, the same reason fuse_write_update_attr() moves it, and count the extension until the folio under it is dirty, for a reply that leaves after it. FUSE_I_SIZE_UNSTABLE cannot serve as the count: it is a single bit and every writer clears it. [hbi: adapted -- the shared-lock write here commits i_size in fuse_write_end() behind the write cursor rather than claiming the whole extension up front, so the count is held across that commit instead of across the write loop.] Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_writepages_fill() runs with the folio locked and, once folio_start_writeback() has run for an earlier folio of the same pass, with those under writeback too. Asking the server for a grant there is the ordering Documentation/filesystems/fuse/fuse-AOP_TRUNCATED_PAGE- reason.txt exists to forbid: no cluster lock may be taken while a page lock is held. The read path has AOP_TRUNCATED_PAGE to unlock and retry with, and ->writepage has nothing of the sort, so the violation sits where the remedy does not reach. Until now only a writeback driven by a revoke handler skipped, and only because that one deadlocks outright. Skip any folio whose grant has gone: put it back on the dirty list and remember the range. fuse_writepages() takes it back once the pass has let go of every folio, and the pass that follows sends it. A data integrity writeback has no later pass, so it goes round again while anything is left deferred; fsync() and close() would otherwise report bytes written that are still only in the page cache. Not from fuse_launder_folio(), which arrives with the folio locked by folio_unmap_invalidate(), nor from a revoke handler, which would ask for the range it is revoking. There the skip simply stands. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_dlm_request_begin() publishes the range asked for, and fuse_dlm_kill_pending() can only test that one, but the commit records what the server granted, which may reach FUSE_DLM_MAX_EXTRA_GRANT either side of it. A revoke processed while the request was on the wire and landing in that excess marks nothing: the request does not overlap it, and the shards hold no bit for it yet because the grant is not recorded. The excess is then recorded over the revoked range and no later NOTIFY takes it back, which is the case the pending list exists to catch. Publish the widest bounds the commit could record as well, and separate the two outcomes. A revoke over the range asked for still kills the grant. One over the excess alone leaves the range asked for recorded and drops the excess, which only costs a re-request. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_writepages_fill() unions every folio it had to skip, and fuse_writepages() asks for the lot in one grant. A pass sweeping a large file skips folios gigabytes apart, so the union grows to the whole sweep and the request covers a range nothing wanted. Stop extending at one shard. The folios left out stay dirty and a later pass asks for them. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
hbirth
requested review from
achhenderson,
cding-ddn,
hazhou-ddn and
yongzech
September 5, 2026 09:44
Collaborator
Author
|
@yongzech thanks for taking a look ... I have just found a data corruption problem in 6.12 ... so there will be another update. It looks like we write pages that are not dirtied. |
A writer confirms its grant and then copies and dirties, and a revoke landing in between is answered while those bytes are in no page cache and on no wire. The flush the revoke runs cannot find them, so they go out later under a fresh grant, after the server has handed the lock to another node. Add a pin: the range a writer is about to dirty, on a list a revoke walks before it removes anything. A revoke publishes the range it takes away on a second list and waits for the pins overlapping it, and a pin overlapping a published range is refused. Refusal and wait test the same overlap, so the wait converges, and ranges that do not overlap never meet: a notify leaves the rest of the file writable and a write outside the notified range does not hold it up. Both nodes are caller storage, so nothing is allocated to take a pin and the writeback path can take one with a folio held. Confirm the grant under the pin, never before, in the three places bytes become the server's: the cached interior of a chunk, the writethrough edges around their FUSE_WRITE, and the writeback folio until it is under writeback. A grant that has gone is asked for again with the pin dropped, since that request is answered by the server the revoke came from. With the writers fenced, the flush a revoke runs needs a single pass: nothing can turn up dirty behind it. [hbi: adapted -- ->write_begin and ->write_end have nowhere to keep a pin node that spans the pair, so the cached interior is pinned once per chunk rather than once per folio, and fuse_dlm_buffered_write() cuts that interior into shard sized chunks so a large write bounds how long it holds a revoke up.] Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A folio made uptodate from the server is served to every later reader, so the grant it was fetched under has to be held from the confirmation until the bytes are in the page cache. Without that a revoke sweeping the range leaves the fill behind it: the folio stays cached, uncovered, and the server sends no further notify for a lock this client no longer holds. Confirm the grant under a pin, as the write path does. Refused, or not covered, unlock the folio and back off with AOP_TRUNCATED_PAGE: neither the wait nor the grant request may be taken with a page lock held. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A readahead reply lands in the page cache from the task that processes it, so the pin over the folios has to span the request: taken before it is sent and dropped once the folios are filled and unlocked. A revoke of the range waits for that and drops the folios after; one already draining refuses the pin and the window goes back unfilled. The node therefore outlives the pinning task, which fuse_dlm_unpin() cannot express. Add a span-owned pin, dropped by node and carrying no owner for the by-owner search to match. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A read grant may not be requested under a page lock, so every buffered read path asks for one before it enters the page cache: buffered read, splice read and read fault. The window covers what readahead may add beyond the read, bounded by the file. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
->readahead runs with every page of the window locked, so the grant request it sent went out under those locks and a revoke of the window had to be given up on. The same goes for the one-request lookahead past the window, which runs from inside ->readahead with the window still in flight. The read now takes the grant before the page cache is entered; fill only what it covers. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
POSIX_FADV_WILLNEED and readahead(2) fill through ->readahead, which now fills only what a grant already covers. Take the grant for the advised range first, with no page held. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A streamed write goes to the server out of the caller's pages, so its bytes are in no page cache and a revoke of the range finds nothing to flush. Hold the grant across the FUSE_WRITE, as the writethrough edges do. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
An O_DIRECT read or write neither fills nor dirties the page cache, so a grant over its range covers nothing and only conflicts with the rest of the cluster. Skip it on both sides. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_readahead() rounds the window mm built up to a whole request and fuse_readahead_lookahead() puts one more request past that, so the pages this client fills reach further than the ra_pages the read asked a grant for. Now that neither of them requests a grant of its own, those pages are the ones fuse_send_readpages() declines: the lookahead allocates a window, hands it straight back unfilled, and the chain it exists to keep alive dies at the first hop. Size the grant from the same unit the window is built in, so what readahead may fill is what the read covers. A wider read grant conflicts only with a remote writer; other readers share it. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_file_open() reads the version once the reply is already back, so it is never older than anything that moved while the request was on the wire and fuse_change_attributes_i() applies the attributes however stale they have become. Under DLM that is a size from before the open shrinking i_size back under the writers. Sample it before the request, the way every other caller does. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
filemap_range_needs_writeback() covers the tail only while it is dirty. A reply that is merely behind always has those folios here but not always dirty: writeback can clean one while the reply that would move the server's size is still on the wire, and the beyond-EOF shortcut in fuse_write_begin() zeroes a folio over data the server does hold. Test for any cached folio instead, and ask the lock tree last since that query sleeps. A remote truncate still gets through, as it revokes first and the revoke drops the range. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_send_writepage() crops a queued request to i_size and ends the writeback on everything past it without sending it. The folios come back clean with their bytes gone and nothing reports an error, so a later fsync() succeeds over the hole. That is what a truncate wants and silent data loss for every other shrink, which upstream never sees because a writeback mount keeps STATX_SIZE. With DLM the server's size is applied, and a reply behind the local writers crops queued data away. Crop against fi->wb_crop instead. A truncate lowers it under the freeze; otherwise it follows i_size back down only once no request is left to protect. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
invalidate_inode_pages2() launders a dirty mapping one folio per round trip, and it waits for replies fuse_flush_writepages() holds back while writepages are frozen. fuse_notify_invalidate_range() already writes the range back in batched writeback and substitutes a plain drop under the freeze. Use it for the attribute change path too, and rename it as it is no longer notify only. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A size that differs from the cached one is upstream's evidence that another client wrote the file. Under DLM the evidence is redundant: a remote write has to revoke the grant covering it, and the revoke invalidates the range on its way through. It is also not affordable. Another node extends a shared file, the GETATTR that fuse_cache_write_iter() takes for STATX_MODE returns the larger size, and fuse_attr_cache_mask() holds STATX_SIZE only for the opposite direction, so a write that wanted the mode bits launders the whole mapping with the writer parked behind it. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_attr_cache_mask() decides whether the server's smaller size may be taken, then sleeps in the grant query before its answer is used. A write below EOF extends nothing, so it bumps neither fi->attr_version, which would drop the reply, nor fi->size_extenders, which would hold the size. Folios dirtied in the doomed range during that window are invisible to all three legs. Re-testing closer to the truncate is not enough on its own. truncate_pagecache() runs after fi->lock is dropped and holds nothing a writer holds, so a folio dirtied between the decision and the walk, or during the walk, is discarded anyway, with no error to report it: the bytes are gone and a later fsync() succeeds over the hole. A real truncate may discard the cache because fuse_set_nowrite() and i_rwsem hold the writers off; an attribute reply holds off nothing. So keep the folios, and re-test the size under fi->lock where nothing sleeps between the answer and its use. If the size was wrong the folios are written back and the size recovers, and fuse_flush_writepages() crops against a high water mark so nothing is clipped meanwhile. If it was right, the revoke that had to precede it already laundered and dropped the range, so there is nothing left to discard. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
A server may acknowledge fewer bytes than the FUSE_WRITE carried. The write path acts on that: fuse_send_write() returns the short count and fuse_perform_write() stops there. Writeback had no caller to report it to and ended every folio clean, so the bytes past the reply were dropped with nothing recording it and a later fsync() succeeded over the hole. Redirty the folios the reply does not reach, so the bytes survive for another pass, and set the error on the mapping: the redirty lands after the wait an fsync() in progress has already done, and that fsync would otherwise return success over them once more. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_writepage_need_send() tests the next alignment against wbc->range_end, a page index against a byte offset. It also reads that field for a cyclic writeback, which does not set it: write_cache_pages() runs to the end of the mapping there and leaves range_end at zero, so the test is true for every aligned index and each folio is sent as a request of its own. Take the bound in page indices, and to the end of the mapping when the pass is cyclic. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
fuse_writeback_kick_stream() reads fi->write_stream_start twice in the same decision, once to round down into the start of the range and again to compare against its end. A writer moving the mark in between inverts the range the kick is given. Racing writers are meant to cost a kick rather than correctness, which needs each mark read once into a local. Take the end from this write's own position rather than reading back the mark just stored, and annotate the accesses. Still no lock: the run is a hint, and nothing waits for it. Signed-off-by: Horst Birthelmer <hbirthelmer@ddn.com>
hbirth
force-pushed
the
redfs-rhel10_0
branch
from
September 5, 2026 22:08
d4906fe to
dce3226
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.